#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import time

from common.common_models import ActionResult
from common.const import ExecuteResultEnum, SubJobStatusEnum, RestoreTypeEnum, ReportDBLabel
from common.logger import Logger
from common.number_const import NumberConst
from db2.backup.util.db2_backup_util import Db2BackupUtil
from db2.comm.const import Db2Const
from db2.comm.db2_exception import ErrCodeException
from db2.comm.error_code import Db2ErrCode
from db2.comm.util.common_util import Db2CommonUtil
from db2.comm.util.dpf_util import DpfUtil
from db2.comm.util.job_decorators import job_exception_decorator
from db2.comm.util.param_util import Db2ParamUtil
from db2.restore.db2_restore_base import Db2RestoreBase
from db2.restore.dpf.dpf_db_restore_service import DpfDbRestoreService

LOGGER = Logger().get_logger(filename="db2.log")


class DpfDbRestore(Db2RestoreBase):
    """Db2数据库恢复任务执行类
    """

    def __init__(self, task_name, pid, job_id, sub_job_id, param_dict):
        super().__init__(task_name, pid, job_id, sub_job_id, param_dict)
        self._os_user = Db2ParamUtil.get_tgt_os_user_when_restore(self.pid)
        self._db_name = Db2ParamUtil.get_db_name_when_restore_db(self.param_dict)
        self._restore_type = Db2ParamUtil.get_restore_type(self.param_dict)

    @job_exception_decorator(is_restore=True)
    def allow_restore_in_local_node(self):
        # 检查源数据库所属实例和目标数据库所属实例是否一致
        src_inst_name = Db2ParamUtil.get_inst_name_of_db_copy(self.param_dict)
        tgt_inst_name = Db2ParamUtil.get_tgt_inst_name_when_restore_db(self.param_dict)
        if not tgt_inst_name or not src_inst_name or tgt_inst_name != src_inst_name:
            LOGGER.error(f"The instance name is inconsistent or does not exist, source instance: {src_inst_name}, "
                         f"target instance: {tgt_inst_name}, job id: {self.job_id}.")
            return Db2CommonUtil.build_action_result(
                message="The instance name is inconsistent or does not exist",
                body_err=Db2ErrCode.DB_INSTANCE_NOT_EQUAL, code=ExecuteResultEnum.INTERNAL_ERROR.value,
                err_params=[src_inst_name, tgt_inst_name])
        # 检查DPF集群各节点状态是否正常
        connect_status = Db2CommonUtil.check_process_service(self._os_user)
        if not connect_status:
            LOGGER.error(f"Check dpf instance connection result: {connect_status}.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, message="The DPF cluster status is abnormal",
                body_err=Db2ErrCode.DB_SERVICE_ERROR)
        # 检查集群节点数是否一致
        src_nodes_num, tgt_nodes_num = Db2ParamUtil.get_nodes_num(self.param_dict)
        if src_nodes_num != tgt_nodes_num:
            LOGGER.error(f"The number of source nodes {src_nodes_num} and target nodes {tgt_nodes_num} not equal")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, body_err=Db2ErrCode.NODES_NUM_NOT_EQUAL,
                message="The nodes of the instance are inconsistent",
                err_params=[str(src_nodes_num), str(tgt_nodes_num)])
        # 检查实例分片信息是否一致
        src_partitions, tgt_partitions = Db2ParamUtil.get_partition_nums(self.param_dict, self._os_user)
        if set(src_partitions) != set(tgt_partitions):
            LOGGER.error(f"The partition numbers of the source and the target instance are inconsistent, source: "
                         f"{src_partitions}, target: {tgt_partitions}, os user: {self._os_user}.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, body_err=Db2ErrCode.INSTANCE_CATALOG_NUM_NOT_EQUAL,
                message="The partition numbers are inconsistent",
                err_params=[str(set(src_partitions)), str(set(tgt_partitions))])
        # 检查数据库编目号
        src_catalog_num, tgt_catalog_num = Db2ParamUtil.get_catalog_num(self.param_dict)
        if src_catalog_num != tgt_catalog_num:
            LOGGER.error(f"The catalog number of the source and the target database are inconsistent, source: "
                         f"{src_catalog_num}, target: {tgt_catalog_num}, os user: {self._os_user}.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, body_err=Db2ErrCode.DATABASE_CATALOG_NUM_NOT_EQUAL,
                message="The catalog number are inconsistent", err_params=[str(src_catalog_num), str(tgt_catalog_num)])
        # 检查目标数据库是否存在
        all_db_names = Db2CommonUtil.get_db_info_dict(self._os_user).keys()
        if self._db_name not in all_db_names:
            LOGGER.error(f"The  database {self._db_name} to restore does not exist, os user: {self._os_user}.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, message="The database to restore does not exist",
                body_err=Db2ErrCode.DATABASE_NOT_EXISTS, err_params=[self._db_name])
        return self.check_db_cfg_for_restore()

    def check_db_cfg_for_restore(self):
        # 目标数据库需开启日志归档
        log_arch_meth_val = Db2CommonUtil.get_log_arch_meth_val_of_db(self._os_user, self._db_name)
        if not log_arch_meth_val or log_arch_meth_val.upper() == "OFF":
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value,
                message="The DPF cluster database does not enable log archive method",
                body_err=Db2ErrCode.ARCHIVE_MODE_DISABLE)
        # 是否存在penging状态的任务（Backup、Rollforward、Restore、Upgrade）
        pending_ret, pending_info_str = DpfUtil.check_database_pending_status_for_restore(self._os_user, self._db_name)
        if pending_ret:
            LOGGER.error("There are pending tasks in the DPF database.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, message="There are pending tasks in the DPF database",
                body_err=Db2ErrCode.DATABASE_EXISTS_PENDING, err_params=[pending_info_str])
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore_pre_job(self):
        LOGGER.info(f"Start executing restore database pre job, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub jod id: {self.sub_job_id}")
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        LOGGER.info(f"Execute restore database pre job finished, pid: {self.pid}, job id: {self.job_id}")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore(self):
        s_job_name = Db2ParamUtil.get_sub_job_name_for_restore(self.param_dict)
        LOGGER.info(f"Start executing restore database subtask: {s_job_name}, pid: {self.pid}, "
                    f"job id: {self.job_id}, sub job id: {self.sub_job_id}.")
        self.sub_job_detail.log_detail = [
            Db2CommonUtil.build_log_detail(ReportDBLabel.RESTORE_SUB_START_COPY, [self.sub_job_id])
        ]
        # 挂载目录子任务，确保执行后续子任务时各节点已成功挂载目录
        if s_job_name == "mount_path_subtask":
            return self.exec_mount_path_subtask()

        self.sub_job_detail.progress = NumberConst.FIFTEEN
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        time.sleep(Db2Const.LABEL_SLEEP_TIME)
        self.sub_job_detail.log_detail = []
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        # 获取编目节点IP
        catalog_ip = Db2ParamUtil.get_catalog_ip_when_restore(self.param_dict)
        if not catalog_ip:
            LOGGER.error("The catalog IP param is empty.")
            self.sub_job_detail.task_status = SubJobStatusEnum.FAILED.value
            Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                     message="The catalog IP param is empty")
        # 非编目节点不做恢复操作
        if not DpfUtil.is_catalog_node(catalog_ip):
            LOGGER.info("Current node is no catalog node, no need execute restore database job.")
            Db2CommonUtil.disconnect_db_application(self._os_user, self._db_name)
            self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
            Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)
        if self._restore_type == RestoreTypeEnum.FULL_RESTORE.value:
            DpfDbRestoreService.exec_full_restore_db(self._os_user, self._db_name, self.param_dict)
        elif self._restore_type == RestoreTypeEnum.INCRE_RESTORE.value:
            DpfDbRestoreService.exec_incr_restore_db(self._os_user, self._db_name, self.param_dict)
        elif self._restore_type == RestoreTypeEnum.DIFF_RESTORE.value:
            DpfDbRestoreService.exec_diff_restore_db(self._os_user, self._db_name, self.param_dict)
        elif self._restore_type == RestoreTypeEnum.LOG_RESTORE.value:
            log_ret = DpfDbRestoreService.exec_log_restore_db(self._os_user, self._db_name, self.param_dict)
            if log_ret:
                self.sub_job_detail.log_detail = [Db2CommonUtil.build_log_detail(log_ret, None)]
                Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
                self.sub_job_detail.log_detail = []
        else:
            LOGGER.error(f"Unsupported restore type: {self._restore_type}.")
            raise ErrCodeException(Db2ErrCode.ERROR_COMMON_INVALID_PARAMETER)
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        LOGGER.info(f"Execute restore database job finished, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub job id: {self.sub_job_id}.")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def exec_mount_path_subtask(self):
        if DpfUtil.is_catalog_node(Db2ParamUtil.get_catalog_ip_when_restore(self.param_dict)):
            # 只在编目节点执行，防止多节点并发执行报错
            Db2CommonUtil.clear_db_archive_dir_before_restore(self._os_user, self._db_name)
        Db2CommonUtil.disconnect_db_application(self._os_user, self._db_name)
        # 全量、增量、差异恢复各节点都需创建NEWLOGPATH
        if self._restore_type in (RestoreTypeEnum.FULL_RESTORE.value, RestoreTypeEnum.INCRE_RESTORE.value,
                                  RestoreTypeEnum.DIFF_RESTORE.value):
            DpfUtil.create_new_log_path_for_restore_db(self._os_user, self._db_name, self.param_dict)
        self.sub_job_detail.progress = NumberConst.TEN
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore_post_job(self):
        LOGGER.info(f"Start executing restore database post job, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub jod id: {self.sub_job_id}.")
        self.sub_job_detail.progress = NumberConst.FIVE
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        # 恢复后置任务
        if self._restore_type in (RestoreTypeEnum.INCRE_RESTORE.value, RestoreTypeEnum.DIFF_RESTORE.value):
            copies = Db2ParamUtil.parse_copies(self.param_dict)
            full_copy_path = Db2ParamUtil.get_full_copy_path_for_incr_and_diff_restore(copies, job_id=self.job_id)
            Db2CommonUtil.clear_soft_link_of_dir(full_copy_path)
            LOGGER.info(f"Remove soft link of database full copy success, restore type: {self._restore_type}.")
        elif self._restore_type == RestoreTypeEnum.LOG_RESTORE:
            copies = Db2ParamUtil.parse_copies(self.param_dict)
            last_log_copy_path = Db2ParamUtil.get_log_copy_paths(copies, job_id=self.job_id)[-1]
            Db2CommonUtil.clear_soft_link_of_dir(last_log_copy_path)
            LOGGER.info(f"Remove soft link of database last log copy success, restore type: {self._restore_type}.")
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        LOGGER.info(f"Execute restore database post job finished, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub job id: {self.sub_job_id}.")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def report_progress(self):
        cache_path = Db2ParamUtil.get_cache_mount_path_for_restore(self.param_dict)
        if self.sub_job_id:
            Db2BackupUtil.report_progress_utl(self.pid, self.sub_job_id, cache_path)
        else:
            Db2BackupUtil.report_progress_utl(self.pid, self.job_id, cache_path)

    def output_action_result(self, action_ret: ActionResult):
        super(DpfDbRestore, self).output_action_result(action_ret)
